%%%-------------------------------------------------------------------
%%% @author Viktor Green <viktor@student.gu.se>
%%% @copyright (C) 2011, Viktor Green
%%% @doc
%%% Central controller which handles new torrents
%%% @end
%%% Created : 14 Nov 2011 by Viktor Green <viktor@student.gu.se>
%%%-------------------------------------------------------------------
-module(torrent_handler).

-behaviour(gen_server).

%% API
-export([start_link/0, add_torrent/1, get_peers/1, connect/2]).

%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
	 terminate/2, code_change/3]).

-define(SERVER, ?MODULE). 

-include("records.hrl").

-include_lib("kernel/include/file.hrl").

-record(state, {torrents = [] :: [{reference(), pid()}] | []
	       ,un_added_torrents = [] :: [ #torrent{}] | []
	       }).

%%%===================================================================
%%% API
%%%===================================================================

%%--------------------------------------------------------------------
%% @doc
%% Starts the server
%%
%% @spec start_link() -> {ok, Pid} | ignore | {error, Error}
%% @end
%%--------------------------------------------------------------------
start_link() ->
    gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).


%%--------------------------------------------------------------------
%% @doc
%% Adds a torrent file to the application to be tracked/downloaded
%% @end
%%--------------------------------------------------------------------
-spec add_torrent(File :: string()) -> {added
					,ID :: reference()
					,Type :: file | directory
				        ,Name :: string()
				        ,Status :: string()}.
add_torrent(File) ->
    gen_server:call(?SERVER, {add_torrent, File}).


%%--------------------------------------------------------------------
%% @doc
%% Request peers from the tracker.
%% The Id provided is the Id returned from the add_torrent function
%% @end
%%--------------------------------------------------------------------
-spec get_peers(Id :: reference()) -> [tracker:address()].
get_peers(Id) ->
    gen_server:call(?SERVER, {get_peers, Id}).


%%--------------------------------------------------------------------
%% @doc
%% Connect to peers
%% @end
%%--------------------------------------------------------------------
-spec connect(Id :: reference(), Peers :: [tracker:adress()]) -> ok.
connect(Id, Peers) ->
    gen_server:cast(?SERVER, {connect, Id, Peers}).

%%%===================================================================
%%% gen_server callbacks
%%%===================================================================

%%--------------------------------------------------------------------
%% @private
%% @doc
%% Initializes the server
%%
%% @spec init(Args) -> {ok, State} |
%%                     {ok, State, Timeout} |
%%                     ignore |
%%                     {stop, Reason}
%% @end
%%--------------------------------------------------------------------
init([]) ->
    {ok, #state{}}.

%%--------------------------------------------------------------------
%% @private
%% @doc
%% Handling call messages
%%
%% @spec handle_call(Request, From, State) ->
%%                                   {reply, Reply, State} |
%%                                   {reply, Reply, State, Timeout} |
%%                                   {noreply, State} |
%%                                   {noreply, State, Timeout} |
%%                                   {stop, Reason, Reply, State} |
%%                                   {stop, Reason, State}
%% @end
%%--------------------------------------------------------------------
handle_call({add_torrent, Path}, _From, State) ->
    {Id, Type, Name, Status, NewState} = new_torrent(Path, State),
    {reply, {added, Id, Type, Name, Status}, NewState};
handle_call({get_peers, TorrentId}, _From, State) ->
    Reply = case get_pid(TorrentId, State) of
		false -> [];
		Pid -> tracker_srv:get_peers(Pid)
	    end,
    {reply, Reply, State};
handle_call({set_target, Ref, Path}, _From, State) ->
    {Status, NewState} = set_target(Ref, Path, State),
    {reply, Status, NewState};
handle_call(_Request, _From, State) ->
    Reply = ok,
    {reply, Reply, State}.

%%--------------------------------------------------------------------
%% @private
%% @doc
%% Handling cast messages
%%
%% @spec handle_cast(Msg, State) -> {noreply, State} |
%%                                  {noreply, State, Timeout} |
%%                                  {stop, Reason, State}
%% @end
%%--------------------------------------------------------------------
handle_cast({connect, TorrentId, Peers}, State) ->
    case get_pid(TorrentId, State) of
	false -> ok;
	Pid ->
	    PeerID = singleton_storage:get_peerid(),
	    connect(Peers, PeerID, tracker_srv:get_info_hash(Pid))
    end,
    {noreply, State};

handle_cast({stop, TorrentId}, State) ->
    case get_pid(TorrentId, State) of
	false -> ok;
	Pid ->
	    exit(Pid, "Stopped by user")
    end,
    {noreply, State};

handle_cast(_Msg, State) ->
    {noreply, State}.

%%--------------------------------------------------------------------
%% @private
%% @doc
%% Handling all non call/cast messages
%%
%% @spec handle_info(Info, State) -> {noreply, State} |
%%                                   {noreply, State, Timeout} |
%%                                   {stop, Reason, State}
%% @end
%%--------------------------------------------------------------------
handle_info(_Info, State) ->
    {noreply, State}.

%%--------------------------------------------------------------------
%% @private
%% @doc
%% This function is called by a gen_server when it is about to
%% terminate. It should be the opposite of Module:init/1 and do any
%% necessary cleaning up. When it returns, the gen_server terminates
%% with Reason. The return value is ignored.
%%
%% @spec terminate(Reason, State) -> void()
%% @end
%%--------------------------------------------------------------------
terminate(_Reason, _State) ->
    ok.

%%--------------------------------------------------------------------
%% @private
%% @doc
%% Convert process state when code is changed
%%
%% @spec code_change(OldVsn, State, Extra) -> {ok, NewState}
%% @end
%%--------------------------------------------------------------------
code_change(_OldVsn, State, _Extra) ->
    {ok, State}.

%%%===================================================================
%%% Internal functions
%%%===================================================================

%%--------------------------------------------------------------------
%% @doc
%% Gets the pid for a torrent's tracker_srv
%% @end
%%--------------------------------------------------------------------
-spec get_pid(TorrentId :: reference(), #state{}) -> pid() | false.
get_pid(TorrentId, #state{torrents = Torrents}) ->
    case lists:keyfind(TorrentId, 1, Torrents) of
                false -> false;
                {TorrentId, Pid} -> Pid
    end.

%%--------------------------------------------------------------------
%% @doc
%% Connect to the peers
%% @end
%%--------------------------------------------------------------------
-spec connect(Destination :: [tracker:address()],
	      PeerId :: string(),
	      InfoHash :: string()) ->
		     ok.
connect([], _, _) ->
    ok;
connect([{IP, Port} | Tail], PeerId, InfoHash) ->
    try connect:handshake(IP, Port, PeerId, InfoHash)
    catch
	error:Thing -> Thing
    end,
    connect(Tail, PeerId, InfoHash).

%%--------------------------------------------------------------------
%% @doc
%% Internal function to read a .torrent file and add it to
%% a suprvisor to track
%% @end
%%--------------------------------------------------------------------
-spec new_torrent(FilePath :: string(), State :: #state{}) ->
			 {ID :: reference(),
			  Type :: file | directory,
			  Name :: string(),
			  Status :: string(),
			  State :: #state{}}.
new_torrent(FilePath, #state{un_added_torrents = Torrents} = State) ->
    {ok, Parsed} = torrent_file:parse(FilePath),
    Info = torrent_file:get_value("info", Parsed),
    {string, Announce} = torrent_file:get_value("announce", Parsed),
    Info_value = bencode:encode_to_binary(Info),
    Info_hash = binary_to_list(crypto:sha(Info_value)),
    ID = make_ref(),
    {string, Name} = torrent_file:get_value("name", Info),
    Type = case torrent_file:get_value("files", Info) of
	       {error, _} -> file;
	       _ -> directory
	   end,
    {integer, Length} = torrent_file:get_value("length", Info),
    {integer, PLength} = torrent_file:get_value("piece length", Info),
    {string, CheckSums} = torrent_file:get_value("pieces", Info),
    Torrent = #torrent{id = ID,
		       announce = Announce,
		       info_hash = Info_hash,
		       length = Length,
		       pieceLength = PLength,
		       checkSums = CheckSums,
		       name = Name,
		       type = Type},
    {ID, Type, Name, "added",
     State#state{un_added_torrents = [Torrent | Torrents]}}.

%%--------------------------------------------------------------------
%% @doc
%% Getting the target for a torrent
%% @end
%%--------------------------------------------------------------------
-spec set_target(Ref :: reference(), Path :: string(),
		 State :: #state{}) ->
			{Status :: string(), NewState :: #state{}}.
set_target(Ref, Path, #state{un_added_torrents = UnTorrents,
			     torrents = Torrents} = State) ->
    case lists:keyfind(Ref, #torrent.id, UnTorrents) of
	false -> {"error", State};
	Torrent ->
	    Size = Torrent#torrent.length,
	    File = filename:join(Path, Torrent#torrent.name),
	    FileExists = case file:read_file_info(File) of
			     {ok, FileInfo} ->
				 case FileInfo#file_info.size of
				     Size -> true;
				     _ -> false
				 end;
			     {error, enoent} ->
				 false
			 end,
	    Start_value = snoopy_torrent_sup:start_torrent_sup(Ref, Torrent,
							       File, FileExists),
	    io:format("~p ~n", [Start_value]),
	    Pid = case Start_value of
		      {ok, Child} -> Child;
		      {ok, Child, _Info} -> Child;
		      {error, _Reason} -> error
		  end,
	    {NewTorr, Status} = case Pid of
				    error -> {Torrents, "error"};
				    _ ->
					case torrent_sup:get_ctrl(Pid) of
					    undefined -> {Torrents, "error"};
					    Ctrl -> {[{Ref, Ctrl} | Torrents], "started"}
					end
				end,
	    NewUnTorrents = lists:keydelete(Ref, #torrent.id, UnTorrents),    
	    {Status, State#state{torrents = NewTorr,
				 un_added_torrents = NewUnTorrents}}
    end.
